using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using NDF.XnaLibrary;
using Microsoft.Xna.Framework.Input;
using NDF.ConnectionLibrary;
using NDF.XnaLibrary.Models;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Threading.Tasks;
using System.Threading;

namespace NDF
{
	public class Game : Microsoft.Xna.Framework.Game
	{
		GraphicsDeviceManager graphics;
		SpriteBatch spriteBatch;

		Character playerCharacter;
		Dictionary<int, Character> otherPlayerCharacters;
		Dictionary<int, bool> isWalking;
		KeyboardState previousKeyboardState;

		SpriteFont font;
		Effect loadedEff;

		Texture2D texture;
		RenderTarget2D shadowMapRenderTarget;
		Texture2D shadowMap;

		Map map;
		bool DrawShadowMapOnly;
		Vector3 light0Pos = new Vector3(200f, 30f, 150f);

		//Networking------------------------------
		TcpClient sendingClient;
		TcpClient receivingClient;
		static int bufferSize = Parser.ServerNotificiationLength;
		Dictionary<int, CharacterInfo> alreadyConnectedPlayers;
		bool connected;

		//A few global variables to reduce the overhead of recreating strings and byte arrays in the networking methods.
		//Two of each, since sending and receiving are asynchronous and are likely happen at the same time.
		//A stringbuilder to ease building messages to be send to the server.
		string textFromServer;
		byte[] dataToServer, dataFromServer;
		StringBuilder textToServerBuilder = new StringBuilder(Parser.ClientInformationLength, Parser.ClientInformationLength);

		public Game()
		{
			graphics = new GraphicsDeviceManager(this);
			Content.RootDirectory = "Content";
		}

		protected override void Initialize()
		{
			Window.AllowUserResizing = true;
			IsFixedTimeStep = false;
			IsMouseVisible = true;

			TextReader file = new StreamReader("ConnectionSettings.txt");
			ServerSettings settings = Parser.ParseServerSettings(file);

			map = new Map(400, 400, 1);
			map.GenerateBasicMap();
			Camera.Active = new Camera(light0Pos);
			
			ConnectToServer(settings);
			//Task.Factory.StartNew(HandleServerData, TaskCreationOptions.LongRunning);

			base.Initialize();
		}

		protected override void LoadContent()
		{
			spriteBatch = new SpriteBatch(GraphicsDevice);
			font = Content.Load<SpriteFont>("SpriteFont1");
			previousKeyboardState = Keyboard.GetState();
			texture = Content.Load<Texture2D>("grass");
			loadedEff = Content.Load<Effect>("NDF-Effect");

			loadedEff.Parameters["PointLight0Position"].SetValue(light0Pos);
			loadedEff.Parameters["PointLight0Color"].SetValue(Color.White.ToVector4());
			loadedEff.Parameters["PointLight0Range"].SetValue(300);
			loadedEff.Parameters["AmbientColor"].SetValue(new Color(0.3f, 0.3f, 0.3f).ToVector4());
			loadedEff.Parameters["Attenuation2"].SetValue(0f);
			loadedEff.Parameters["Attenuation1"].SetValue(0.01f);
			loadedEff.Parameters["Attenuation0"].SetValue(0f);

			//Init characters
			otherPlayerCharacters = new Dictionary<int,Character>();
			isWalking = new Dictionary<int,bool>();
			
			playerCharacter = GetDefaultCharacter();
			playerCharacter.Rotating += HandleRotation;
			playerCharacter.Moving += HandleMovement;
			playerCharacter.Casting += HandleCasting;
			
			//Init already connected players
			foreach (var item in alreadyConnectedPlayers)
			{
				AddNewPlayerCharacter(item.Key, item.Value);
			}
			alreadyConnectedPlayers = null;

			Camera.Active.FarPlane = 10000;

			PresentationParameters pp = GraphicsDevice.PresentationParameters;
			shadowMapRenderTarget = new RenderTarget2D(GraphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight, true, GraphicsDevice.DisplayMode.Format, graphics.PreferredDepthStencilFormat);

		}		

		#region Networking

		private void ConnectToServer(ServerSettings settings)
		{
			sendingClient = new TcpClient();
			receivingClient = new TcpClient();

			sendingClient.Connect(new IPEndPoint(settings.IP, settings.ClientToServerPort));
			receivingClient.Connect(new IPEndPoint(settings.IP, settings.ServerToClientPort));
			//Use 1024 as the buffer size since we are receiving a dictionary, not a ServerNotification
			dataFromServer = ConnectionHelper.ReadData(receivingClient, 1024);
			textFromServer = Parser.EncodingFormat.GetString(dataFromServer).Trim().Replace("\0", "");

			alreadyConnectedPlayers = Parser.ParseMultipleCharacterInformation(new StringReader(textFromServer));
			connected = true;
		}

		private void HandleRotation(object source, RotatingEventArgs args)
		{
			textToServerBuilder.Append(Parser.StringifyQuaternion(args.Rotation, "Rotating"));
			SendInformationToServer();
		}

		private void HandleMovement(object source, WalkingEventArgs args)
		{
			textToServerBuilder.Append(Parser.StringifyVector3(args.Direction_Position, "Walking"));
			SendInformationToServer();
		}

		private void HandleCasting(object source, CastingEventArgs args)
		{
			textToServerBuilder.Append("Casting: " + args.Skill.Id);
			SendInformationToServer();
		}

		private void SendInformationToServer()
		{
			if (textToServerBuilder.Length < textToServerBuilder.MaxCapacity)
			{
				textToServerBuilder.Append("\n");
				while(textToServerBuilder.Length < textToServerBuilder.MaxCapacity)
				{
					textToServerBuilder.Append(" ");
				}
			}
			try
			{
				dataToServer = Parser.EncodingFormat.GetBytes(textToServerBuilder.ToString());
				ConnectionHelper.SendData(sendingClient, dataToServer);
			}
			catch (Exception e)
			{
				if (e is IOException || e is ObjectDisposedException)
				{
					connected = false;
				}
				else
					throw;
			}
			textToServerBuilder.Clear();
		}

		private void HandleServerData()
		{
			try
			{
				while (receivingClient.Available > 0)
				{
					dataFromServer = ConnectionHelper.ReadData(receivingClient, bufferSize);
					textFromServer = Parser.EncodingFormat.GetString(dataFromServer);
					ServerNotification notification = Parser.ParseSingleCharacterInformation(new StringReader(textFromServer));
					switch (notification.NotificationType)
					{
						case NotificationType.Rotating:
							otherPlayerCharacters[notification.ID].Rotation = (Quaternion)notification.Data;
							break;
						case NotificationType.Walking:
							isWalking[notification.ID] = !isWalking[notification.ID];
							Vector3 direction = (Vector3) notification.Data;
							if (direction.LengthSquared() == 1)
								otherPlayerCharacters[notification.ID].StartWalking(direction);
							else
							{	
								otherPlayerCharacters[notification.ID].StopWalking();
								otherPlayerCharacters[notification.ID].Position = direction;
							}
							break;
						case NotificationType.Casting:
							otherPlayerCharacters[notification.ID].Cast((int)notification.Data);
							break;
						case NotificationType.Connected :
							otherPlayerCharacters.Add(notification.ID, GetDefaultCharacter());
							isWalking.Add(notification.ID, false);
							break;
						case NotificationType.Disconnected:
							otherPlayerCharacters.Remove(notification.ID);
							isWalking.Remove(notification.ID);
							break;
					}
				}
			}
			catch (Exception e)
			{
				if (e is IOException || e is ObjectDisposedException)
				{
					connected = false;
				}
				else
					throw;
			}
		}

		//Method for easing adding player characters
		private void AddNewPlayerCharacter(int id, CharacterInfo info)
		{
			Character newCharacter = GetDefaultCharacter();
			newCharacter.Position = info.Position;
			newCharacter.Rotation = info.Rotation;

			if ((info.State == CharacterState.Casting || info.State == CharacterState.PostCasting ||
				info.State == CharacterState.MovingAndPostCasting) &&
				newCharacter.SpellBook.HasLearnedSkill(info.CastingActionInfo.SkillId)) ;
			{
				newCharacter.Cast(info.CastingActionInfo.SkillId);
			}

			otherPlayerCharacters.Add(id, newCharacter);
			isWalking.Add(id, false);
		}

		#endregion

		protected override void UnloadContent()
		{ }

		protected override void OnExiting(object sender, EventArgs args)
		{
			textToServerBuilder.Append("Disconnected: User disconnect");
			SendInformationToServer();

			sendingClient.Close();
			receivingClient.Close();

			base.OnExiting(sender, args);
		}

		protected override void Update(GameTime gameTime)
		{
			if (connected)
				HandleServerData();
			HandleKeyboard();

			playerCharacter.Update(gameTime);
			foreach (var player in otherPlayerCharacters)
			{
				player.Value.Update(gameTime);
			}

			base.Update(gameTime);
		}

		private void HandleKeyboard()
		{
			KeyboardState currentKeyboardState = Keyboard.GetState();

			if (currentKeyboardState.IsKeyDown(Keys.K) && previousKeyboardState.IsKeyUp(Keys.K))
				DrawShadowMapOnly = !DrawShadowMapOnly;

			if (currentKeyboardState.IsKeyDown(Keys.D))
				Camera.Active.Pan(1f);
			if (currentKeyboardState.IsKeyDown(Keys.A))
				Camera.Active.Pan(-1f);
			if (currentKeyboardState.IsKeyDown(Keys.W))
				Camera.Active.Tilt(1f);
			if (currentKeyboardState.IsKeyDown(Keys.S))
				Camera.Active.Tilt(-1f);
			if (currentKeyboardState.IsKeyDown(Keys.Space))
				Camera.Active.Move(Vector3.Forward);

			if (currentKeyboardState.IsKeyDown(Keys.NumPad8))
				playerCharacter.Rotation *= Quaternion.CreateFromAxisAngle(Vector3.Right, MathHelper.Pi / 180f);

			#region Movement
			bool left = currentKeyboardState.IsKeyDown(Keys.Left),
				right = currentKeyboardState.IsKeyDown(Keys.Right),
				up = currentKeyboardState.IsKeyDown(Keys.Up),
				down = currentKeyboardState.IsKeyDown(Keys.Down);

			if (playerCharacter.State == CharacterState.Moving || playerCharacter.State == CharacterState.MovingAndPostCasting)
			{
				if (up && playerCharacter.WalkingDirection != Vector3.Forward)
					playerCharacter.StartWalking(Vector3.Forward);
				else if (down && playerCharacter.WalkingDirection != Vector3.Backward)
					playerCharacter.StartWalking(Vector3.Backward);
				else if (right && playerCharacter.WalkingDirection != Vector3.Right)
					playerCharacter.StartWalking(Vector3.Right);
				else if (left && playerCharacter.WalkingDirection != Vector3.Left)
					playerCharacter.StartWalking(Vector3.Left);
				else if (!up && !down && !right && !left)
					playerCharacter.StopWalking();				
			}
			else
			{
				if (currentKeyboardState.IsKeyDown(Keys.Up))
				{
					playerCharacter.StartWalking(Vector3.Forward);
				}
				else if (currentKeyboardState.IsKeyDown(Keys.Down))
				{
					playerCharacter.StartWalking(Vector3.Backward);
				}
				else if (currentKeyboardState.IsKeyDown(Keys.Right))
				{
					playerCharacter.StartWalking(Vector3.Right);
				}
				else if (currentKeyboardState.IsKeyDown(Keys.Left))
				{
					playerCharacter.StartWalking(Vector3.Left);
				}
			}
			#endregion

			if (currentKeyboardState.IsKeyDown(Keys.T) && previousKeyboardState.IsKeyUp(Keys.T))
				playerCharacter.Cast(Skill.GetBasicSkill().Id);

			//Throw an exception to test exception handling
			if (currentKeyboardState.IsKeyDown(Keys.End))
				throw new Exception("Testing exception handling");

			previousKeyboardState = currentKeyboardState;
		}

		public Character GetDefaultCharacter()
		{
			Character character = new Character(Content.Load<Model>("dude"), loadedEff, Vector3.Zero,
				Quaternion.Identity, new Vector3(0.1f), "sande");

			Equipment bat = 
				new Equipment(0, EquipmentType.OneHandedWeapon, "bat", Content.Load<Model>("baseballbat")) 
				{ Scale = new Vector3(10f) };
			character.Inventory.EquipItem(EquipmentSlot.WeaponSlot1, bat);
			character.Position = new Vector3(200, 1, 50);
			character.CombatStats.MovementSpeed = 0.2f;
			character.SaveEffectChanges();

			return character;
		}

		protected override void Draw(GameTime gameTime)
		{
			if (DrawShadowMapOnly)
			{
				GraphicsDevice.Clear(Color.White);

				DrawShadowScene();
			}
			else
			{
				GraphicsDevice.SetRenderTarget(shadowMapRenderTarget);
				GraphicsDevice.Clear(Color.White);

				DrawShadowScene();

				GraphicsDevice.SetRenderTarget(null);
				shadowMap = (Texture2D)shadowMapRenderTarget;
				GraphicsDevice.Clear(Color.Black);

				DrawScene();
			}

			PrintDebuggingInfo();

			

			base.Draw(gameTime);
		}

		private void DrawScene()
		{
			loadedEff.CurrentTechnique = loadedEff.Techniques["TexturedWithShadows"];
			loadedEff.Parameters["View"].SetValue(Camera.Active.ViewMatrix);
			loadedEff.Parameters["Projection"].SetValue(Camera.Active.ProjectionMatrix);
			loadedEff.Parameters["World"].SetValue(Matrix.Identity);
			loadedEff.Parameters["Texture0"].SetValue(texture);
			loadedEff.Parameters["ShadowMapTexture"].SetValue(shadowMap);
			playerCharacter.SaveEffectChanges();
			foreach (var player in otherPlayerCharacters.Values)
			{
				player.SaveEffectChanges();
			}

			RasterizerState rs = new RasterizerState();
			rs.CullMode = CullMode.CullClockwiseFace;
			GraphicsDevice.RasterizerState = rs;
			map.Draw(loadedEff);

			rs = new RasterizerState();
			rs.CullMode = CullMode.None;
			GraphicsDevice.RasterizerState = rs;
			playerCharacter.Draw();
			foreach (var player in otherPlayerCharacters.Values)
			{
				player.Draw();
			}
		}

		private void DrawShadowScene()
		{
			GraphicsDevice.DepthStencilState = DepthStencilState.Default;
			GraphicsDevice.BlendState = BlendState.Opaque;

			RasterizerState rs = new RasterizerState();
			Vector3 lightTarget = new Vector3(15, 0, 0);
			Matrix light = Matrix.CreateLookAt(light0Pos, lightTarget, Vector3.Up) *
						 Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver2, 1, 1, 1000);

			loadedEff.Parameters["LightViewProjection"].SetValue(light);
			loadedEff.CurrentTechnique = loadedEff.Techniques["ShadowMap"];
			loadedEff.Parameters["World"].SetValue(Matrix.Identity);
			map.Draw(loadedEff);

			rs.CullMode = CullMode.None;
			GraphicsDevice.RasterizerState = rs;

			playerCharacter.DrawShadowMap();
			foreach (var player in otherPlayerCharacters.Values)
			{
				player.DrawShadowMap();
			}

			rs = new RasterizerState();
			rs.CullMode = CullMode.CullClockwiseFace;
			GraphicsDevice.RasterizerState = rs;
		}

		private void PrintDebuggingInfo()
		{
			spriteBatch.Begin();

			Vector2 pos = Vector2.Zero;

			SkinnedModelInfo mi = playerCharacter.Model.Tag as SkinnedModelInfo;
			Color color = Color.White;

			pos.Y += font.MeasureString("A").Y;
			spriteBatch.DrawString(font, "Cam Position: " + Camera.Active.Position, pos, color);
			pos.Y += font.MeasureString("A").Y;
			spriteBatch.DrawString(font, "Dude Position: " + playerCharacter.Position, pos, color);
			foreach(var player in otherPlayerCharacters)
			{
			pos.Y += font.MeasureString("A").Y;
			 
			spriteBatch.DrawString(font, "2nd Dude Position: " + player.Value.Position, pos, color);
			}
			pos.Y += font.MeasureString("A").Y;
			if (!connected)
				spriteBatch.DrawString(font, "You are no longer connected to the server", pos, color);


			spriteBatch.End();

			GraphicsDevice.DepthStencilState = new DepthStencilState();
		}
	}
}
